/**
 * 观察者模式又叫发布订阅模式（Publish/Subscribe），它定义了一种一对多的关系,
 * 让多个观察者对象同时监听某一个主题对象
 * 主题对象的状态发生变化时就会通知所有的观察者对象，使得它们能够自动更新自己
 * 观察者的使用场合就是：当一个对象的改变需要同时改变其它对象，并且它不知道具体有多少对象需要改变的时候，就应该考虑使用观察者模式。
 */

/**
 * 样例1：
 * 观察者模式,大体上是：
 * 1、松耦合的代码；
 * 2、一对多的关系；
 * 3、主体状态变化时，所有依赖被通知；
 * 4、主体和观察者互不知晓。
 */

// 发布者
let pub = {
  actions: function(){
    console.log('欢迎订阅!')
    dep.update();
  }
};
//订阅者
let sub1 = {
  update: function(){
    console.log('订阅1');
  }
};
let sub2 = {
  update: function(){
    console.log('订阅2');
  }
}; 
let sub3 = {
  update: function(){
    console.log('订阅3');
  }
};
// 主体(构造函数)
function Dep(_subs){
  this.update = function(){
    for(let i =0; i < _subs.length; i++){
      _subs[i].update();
    }    
  }
}
// 发布消息(即触发执行)
let dep = new Dep([sub1, sub2, sub3]);
pub.actions();
/**
 * 运行说明如下：
 * 1、发布对象pub已经存在；
 * 2、订阅对象[sub1,sub2,sub3]也已经存在；
 * 3、然后主体Dep函数...pub.actions()来发布消息；
 * 4、触发订阅执行update()方法；
 */
/********************************************************************************************************************/

/**
 * 样例2：
 * 事件模型 实际上也是一种发布订阅模式
 * 如：
 */
// 发布者
var pub = function () {
  console.log('欢迎订阅');
}
// 订阅者
var sub = document.body;
// 订阅者实现订阅（订阅者可无限添加订阅，发布都也能随意修改，订阅者与发布者之前无任何关系）
sub.addEventListener('click', pub, false);

/********************************************************************************************************************/

/**
 * 样例3：
 * 自定义实现发布订阅模式
 * 以预定手机为例
 */
// 商家：发布者
var merchants = {};
// 商家的预定列表（缓存预定手机用户的手机号）
merchants.orderList = {};
// 将增加的预订者添加到商家预定客户列表中
merchants.listen = function(id, info){
  // 只有当预订者的手机号不在预定列表中时才添加（手机号为key，info为订阅者的预定信息）
  if(!this.orderList[id]){
    this.orderList[id] = [];
  }
  this.orderList[id].push(info);
  console.log('用户'+ id +'预定成功！');
}
// 将取消预定的手机号从预定列表中移除
merchants.remove = function (id, fn) {
  let infos = this.orderList[id];
  if(!infos || infos.length === 0){
    return false;
  }
  if(!fn){
    return infos && (infos.length === 0);
  }else{
    infos.forEach( (item,i) => {
      if(item === fn){
        infos.splice(i, 1);
        console.log('用户'+ id +'取消了预定！');
      }
    });
  }
}
// 商家发布消息
merchants.publish = function () {
  let id = Array.prototype.shift.call(arguments);
  let infos = this.orderList[id];
  // 判断是否有该用户的预定信息
  if(!infos || infos.length === 0){
    console.log('您还没有预定！');
    return false;
  }
  // 如果有预定信息，则循环通知
  infos.forEach(item => {
    console.log('尊敬的客户：'+ id + '您预定的');
    item.apply(this, arguments);
    console.log('已经到货了');
  });
}

// 用户(订阅者)：预定手机
let customerA = function () {
  console.log('一台黑色手机');
}
let customerB = function () {
  console.log('一台金色手机');
}
let customerC = function () {
  console.log('一台蓝色手机');
}
// 用户预定手机，并留下预约电话
merchants.listen('15888881111', customerA);
merchants.listen('15888882222', customerB);
merchants.listen('15888883333', customerC);

// 用户A在等待过程中取消了预定
merchants.remove('15888881111', customerA);

// 等商家发布通知信息后，用户均接收到了商家的信息。
merchants.publish('15888881111');
merchants.publish('15888882222');
merchants.publish('15888883333');

/**
 * 正常情况是：订阅者先订阅，然后接收发布者的通知消息。
 * QQ的离线模式：是发布者先发布一条消息，再将信息先存储起来，然后等订阅者接收。
 * 推模型和拉模型：
 * 推模型在事件发生时，发布者会将变化状态和数据都推送给订阅者
 * 拉模型在事件发生时，发布者只会给订阅者一个状态改变通知，订阅者会根据发布者提供的接口主动拉取数据。
 */